#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SentinelScan - 企业级高危端口与漏洞自动化审计工具
版本: 3.0 (完整版)
环境: Kali Linux 2025.x / Python 3.10+
作者: xuekn
描述: 集成端口扫描、服务指纹识别、NSE漏洞脚本引擎、未授权访问检测与HTML报告生成。
"""

import os
import sys
import subprocess
import socket
import datetime
import xml.etree.ElementTree as ET
import json
import shutil
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import signal
import threading
import time

# --- 颜色定义 ---
class Colors:
    HEADER = '\033[95m'
    BLUE = '\033[94m'
    GREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    OKGREEN = '\033[92m'

# --- 高危端口策略库 ---
# 定义端口对应的服务、风险描述以及需要触发的Nmap脚本
PORT_STRATEGIES = {
    21: {"service": "FTP", "scripts": "ftp-anon,ftp-vuln*", "risk": "高", "desc": "FTP服务，可能存在匿名登录或明文传输。"},
    22: {"service": "SSH", "scripts": "ssh2-enum-algos", "risk": "中", "desc": "SSH服务，需检查若加密算法和暴力破解风险。"},
    23: {"service": "Telnet", "scripts": "telnet-encryption", "risk": "极高", "desc": "Telnet明文传输协议，极易被嗅探，必须关闭。"},
    80: {"service": "HTTP", "scripts": "http-title,http-headers", "risk": "低", "desc": "Web服务，需进一步进行Web漏洞扫描。"},
    135: {"service": "RPC", "scripts": "msrpc-enum", "risk": "高", "desc": "Windows RPC，可能泄露系统信息或被利用进行DCOM攻击。"},
    139: {"service": "NetBIOS", "scripts": "smb-security-mode", "risk": "高", "desc": "NetBIOS会话服务，辅助SMB攻击。"},
    445: {"service": "SMB", "scripts": "smb-protocols,smb-security-mode,smb-os-discovery,smb-vuln-ms17-010", "risk": "极高", "desc": "SMB文件共享，易受永恒之蓝(MS17-010)等勒索病毒攻击。"},
    1433: {"service": "MSSQL", "scripts": "ms-sql-info,ms-sql-empty-password", "risk": "高", "desc": "SQL Server，可能存在空口令或Sa弱口令。"},
    1521: {"service": "Oracle", "scripts": "oracle-sid-brute", "risk": "高", "desc": "Oracle数据库，TNS监听易受攻击。"},
    3306: {"service": "MySQL", "scripts": "mysql-empty-password,mysql-info", "risk": "高", "desc": "MySQL数据库，检查是否允许外网连接及弱口令。"},
    3389: {"service": "RDP", "scripts": "rdp-vuln-ms12-020", "risk": "高", "desc": "远程桌面，易受BlueKeep等漏洞影响及暴力破解。"},
    5900: {"service": "VNC", "scripts": "vnc-info,realvnc-auth-bypass", "risk": "高", "desc": "VNC远程控制，通常缺乏加密。"},
    6379: {"service": "Redis", "scripts": "redis-info", "risk": "高", "desc": "Redis数据库，未授权访问可导致服务器直接被拿shell。"},
    8080: {"service": "HTTP-Proxy", "scripts": "http-title", "risk": "中", "desc": "常见Tomcat/Jenkins端口，检查管理后台。"},
    27017: {"service": "MongoDB", "scripts": "mongodb-info", "risk": "高", "desc": "MongoDB，默认配置可能无身份验证。"}
}

class ProgressTracker:
    """进度跟踪器"""
    def __init__(self, total_steps):
        self.total_steps = total_steps
        self.current_step = 0
        self.start_time = time.time()
        self.lock = threading.Lock()
    
    def update(self, step_name, current=0, total=0):
        """更新进度"""
        with self.lock:
            self.current_step += 1
            elapsed = time.time() - self.start_time
            progress = min(100.0, (self.current_step / self.total_steps) * 100)
            
            if total > 0:
                print(f"\r[进度: {progress:.1f}%] {step_name} ({current}/{total}) - 耗时: {elapsed:.1f}s", end='', flush=True)
            else:
                print(f"\r[进度: {progress:.1f}%] {step_name} - 耗时: {elapsed:.1f}s", end='', flush=True)

class SentinelScan:
    def __init__(self, target, output_dir="sentinel_scan_results", fast_mode=False):
        """
        初始化扫描器
        :param target: 目标IP或域名
        :param output_dir: 报告输出目录
        :param fast_mode: 是否使用快速模式
        """
        self.target = target
        self.output_dir = output_dir
        self.fast_mode = fast_mode
        self.nmap_bin = shutil.which("nmap")
        self.xml_report = os.path.join(output_dir, f"nmap_{target.replace('/', '_')}.xml")
        self.html_report = os.path.join(output_dir, f"report_{target.replace('/', '_')}.html")
        self.full_xml_report = os.path.join(output_dir, f"nmap_full_{target.replace('/', '_')}.xml")
        self.findings = []
        self.all_open_ports = []
        self.interrupted = False
        self.progress = ProgressTracker(4)  # 总共4个步骤
        
        if not self.nmap_bin:
            print(f"{Colors.FAIL}[!] 错误: 未检测到 Nmap，请先安装: sudo apt install nmap{Colors.ENDC}")
            sys.exit(1)
            
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        
        # 设置信号处理
        signal.signal(signal.SIGINT, self.signal_handler)

    def signal_handler(self, signum, frame):
        """信号处理函数"""
        print(f"\n\n{Colors.WARNING}[!] 检测被用户中断...{Colors.ENDC}")
        self.interrupted = True
        print(f"{Colors.WARNING}[*] 正在保存当前结果...{Colors.ENDC}")
        sys.exit(0)

    def _resolve_target_ip(self):
        """解析目标IP"""
        try:
            ip = socket.gethostbyname(self.target)
            return ip
        except:
            print(f"{Colors.FAIL}[!] 无法解析目标地址: {self.target}{Colors.ENDC}")
            return None

    def _check_redis_unauth(self, ip, port=6379):
        """Python原生检测 Redis 未授权访问"""
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(3)
            s.connect((ip, port))
            s.send(b"INFO\r\n")
            response = s.recv(1024)
            s.close()
            if b"redis_version" in response:
                return True, "发现 Redis 未授权访问漏洞！可以直接获取敏感信息。"
        except:
            pass
        return False, None

    def _check_ssh_weak_auth(self, ip, port=22):
        """简单的SSH弱认证检测"""
        try:
            import paramiko
            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            
            # 尝试常见的默认凭据
            default_credentials = [
                ("root", "root"), ("root", "password"), ("admin", "admin"),
                ("admin", "password"), ("user", "user"), ("guest", "guest")
            ]
            
            for username, password in default_credentials:
                try:
                    client.connect(ip, port=port, username=username, password=password, timeout=5)
                    client.close()
                    return True, f"发现SSH弱认证: {username}:{password}"
                except:
                    continue
            
            client.close()
        except ImportError:
            pass  # paramiko未安装
        except:
            pass
        return False, None

    def _check_mysql_unauth(self, ip, port=3306):
        """检测MySQL未授权访问"""
        try:
            import pymysql
            conn = pymysql.connect(host=ip, port=port, user='', passwd='', connect_timeout=5)
            conn.close()
            return True, "发现MySQL未授权访问漏洞！"
        except:
            pass
        return False, None

    def _run_full_port_scan(self):
        """运行完整的端口扫描"""
        print(f"{Colors.HEADER}[*] 执行完整端口扫描 - 目标: {self.target}{Colors.ENDC}")
        
        # 扫描所有端口
        cmd = [
            self.nmap_bin,
            "-p-",  # 扫描所有端口
            "-sV",  # 版本探测
            "-O",   # 操作系统检测
            "-sS",  # SYN扫描
            "-T4",  # 时间模板
            "--host-timeout", "30m",  # 超时设置
            "--max-retries", "3",     # 最大重试次数
            "-oX", self.full_xml_report,
            self.target
        ]
        
        print(f"{Colors.BLUE}[*] 执行完整端口扫描: {' '.join(cmd)}{Colors.ENDC}")
        
        try:
            result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=1800)  # 30分钟超时
            return True
        except subprocess.TimeoutExpired:
            print(f"{Colors.WARNING}[!] 完整端口扫描超时{Colors.ENDC}")
            return False
        except subprocess.CalledProcessError as e:
            print(f"{Colors.FAIL}[!] 完整端口扫描失败: {e.stderr}{Colors.ENDC}")
            return False

    def _run_targeted_scan(self):
        """运行针对高危端口的扫描"""
        print(f"{Colors.HEADER}[*] 执行高危端口深度扫描 - 目标: {self.target}{Colors.ENDC}")
        
        # 构建高危端口列表
        high_risk_ports = ",".join(map(str, PORT_STRATEGIES.keys()))
        
        # 针对高危端口的深度扫描
        cmd = [
            self.nmap_bin,
            "-sV", "-Pn", "--open", "-T4", 
            "-p", high_risk_ports,
            "--script", "vuln,default,safe",
            "-oX", self.xml_report,
            self.target
        ]
        
        print(f"{Colors.BLUE}[*] 执行高危端口扫描: {' '.join(cmd)}{Colors.ENDC}")
        
        try:
            result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            return True
        except subprocess.CalledProcessError as e:
            print(f"{Colors.FAIL}[!] 高危端口扫描失败: {e.stderr}{Colors.ENDC}")
            return False

    def _parse_full_scan_results(self):
        """解析完整端口扫描结果"""
        if not os.path.exists(self.full_xml_report):
            return

        try:
            tree = ET.parse(self.full_xml_report)
            root = tree.getroot()
            
            for host in root.findall('host'):
                ip_addr = host.find('address').get('addr')
                ports = host.find('ports')
                
                if ports is None: 
                    continue

                for port in ports.findall('port'):
                    state = port.find('state').get('state')
                    if state != 'open': 
                        continue
                    
                    port_id = int(port.get('portid'))
                    service = port.find('service')
                    product = service.get('product', 'Unknown') if service is not None else 'Unknown'
                    version = service.get('version', '') if service is not None else ''
                    full_service = f"{product} {version}".strip()
                    
                    # 添加到所有开放端口列表
                    self.all_open_ports.append({
                        'port': port_id,
                        'service': service.get('name', 'Unknown') if service is not None else 'Unknown',
                        'product': product,
                        'version': version,
                        'state': state
                    })

        except Exception as e:
            print(f"{Colors.FAIL}[!] 解析完整扫描结果出错: {e}{Colors.ENDC}")

    def _parse_targeted_results(self):
        """解析针对高危端口的扫描结果"""
        if not os.path.exists(self.xml_report):
            return

        try:
            tree = ET.parse(self.xml_report)
            root = tree.getroot()
            
            for host in root.findall('host'):
                ip_addr = host.find('address').get('addr')
                ports = host.find('ports')
                
                if ports is None: 
                    continue

                for port in ports.findall('port'):
                    state = port.find('state').get('state')
                    if state != 'open': 
                        continue
                    
                    port_id = int(port.get('portid'))
                    service = port.find('service')
                    product = service.get('product', 'Unknown') if service is not None else 'Unknown'
                    version = service.get('version', '') if service is not None else ''
                    full_service = f"{product} {version}".strip()

                    # 获取 Nmap 脚本输出
                    script_outputs = []
                    for script in port.findall('script'):
                        sid = script.get('id')
                        out = script.get('output')
                        script_outputs.append(f"[{sid}] {out}")

                    risk_info = PORT_STRATEGIES.get(port_id, {"risk": "未知", "desc": "非标准端口", "service": "Unknown"})
                    
                    finding = {
                        "port": port_id,
                        "service_name": risk_info["service"],
                        "banner": full_service,
                        "risk_level": risk_info["risk"],
                        "description": risk_info["desc"],
                        "scripts": script_outputs, # Nmap 脚本发现的问题
                        "manual_verify": []
                    }

                    # --- Python 原生二次验证 ---
                    # Redis 验证
                    if port_id == 6379:
                        is_vuln, msg = self._check_redis_unauth(ip_addr)
                        if is_vuln:
                            finding["manual_verify"].append(msg)
                            finding["risk_level"] = "CRITICAL (已验证)"

                    # SSH 验证
                    if port_id == 22:
                        is_vuln, msg = self._check_ssh_weak_auth(ip_addr)
                        if is_vuln:
                            finding["manual_verify"].append(msg)
                            finding["risk_level"] = "HIGH (已验证)"

                    # MySQL 验证
                    if port_id == 3306:
                        is_vuln, msg = self._check_mysql_unauth(ip_addr)
                        if is_vuln:
                            finding["manual_verify"].append(msg)
                            finding["risk_level"] = "HIGH (已验证)"

                    self.findings.append(finding)

        except Exception as e:
            print(f"{Colors.FAIL}[!] 解析高危端口扫描结果出错: {e}{Colors.ENDC}")

    def run_comprehensive_scan(self):
        """运行综合扫描"""
        print(f"\n{Colors.HEADER}[*] 开始综合安全扫描: {self.target}{Colors.ENDC}")
        print("="*60)
        
        # 步骤1: 解析目标IP
        resolved_ip = self._resolve_target_ip()
        if not resolved_ip:
            print(f"{Colors.FAIL}[!] 无法解析目标IP，扫描终止{Colors.ENDC}")
            return
        
        print(f"{Colors.BLUE}[*] 目标IP解析成功: {resolved_ip}{Colors.ENDC}")
        self.progress.update("解析目标IP")
        
        # 步骤2: 执行完整端口扫描
        if not self.interrupted:
            success = self._run_full_port_scan()
            if success:
                self._parse_full_scan_results()
            self.progress.update("完整端口扫描")
        
        # 步骤3: 执行高危端口深度扫描
        if not self.interrupted:
            success = self._run_targeted_scan()
            if success:
                self._parse_targeted_results()
            self.progress.update("高危端口扫描")
        
        # 步骤4: 生成报告
        if not self.interrupted:
            report_path = self._generate_html_report()
            print(f"\n{Colors.OKGREEN}[+] 完整 HTML 报告已生成: {report_path}{Colors.ENDC}")
            print(f"{Colors.OKGREEN}[+] XML 原始数据: {self.xml_report}{Colors.ENDC}")
            print(f"{Colors.OKGREEN}[+] 完整XML数据: {self.full_xml_report}{Colors.ENDC}")
            self.progress.update("生成报告")
        
        print(f"\n{Colors.BOLD}扫描完成!{Colors.ENDC}")

    def _generate_html_report(self):
        """生成现代化的 HTML 报告"""
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        
        html_content = f"""
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <title>SentinelScan 安全审计报告 - {self.target}</title>
            <style>
                body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f9; margin: 0; padding: 20px; }}
                h1 {{ color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }}
                h2 {{ color: #2c3e50; margin-top: 30px; }}
                .summary {{ background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; }}
                table {{ width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; }}
                th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }}
                th {{ background-color: #34495e; color: #fff; }}
                tr:hover {{ background-color: #f1f1f1; }}
                .tag {{ padding: 4px 8px; border-radius: 4px; color: #fff; font-size: 0.85em; font-weight: bold; }}
                .risk-CRITICAL {{ background-color: #c0392b; }}
                .risk-极高 {{ background-color: #e74c3c; }}
                .risk-高 {{ background-color: #e67e22; }}
                .risk-中 {{ background-color: #f1c40f; color: #333; }}
                .risk-低 {{ background-color: #2ecc71; }}
                pre {{ background: #2d3436; color: #dfe6e9; padding: 10px; border-radius: 4px; white-space: pre-wrap; font-size: 0.9em; }}
                .footer {{ text-align: center; margin-top: 20px; color: #7f8c8d; }}
                .section {{ background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; }}
            </style>
        </head>
        <body>
            <h1>🛡️ SentinelScan 安全审计报告</h1>
            <div class="summary">
                <h2>扫描摘要</h2>
                <p><strong>扫描目标:</strong> {self.target}</p>
                <p><strong>解析IP:</strong> {socket.gethostbyname(self.target) if self.target else 'N/A'}</p>
                <p><strong>生成时间:</strong> {timestamp}</p>
                <p><strong>扫描状态:</strong> {'完成' if not self.interrupted else '被中断'}</p>
                <p><strong>包含策略:</strong> 深度NSE脚本检测 + Python原生验证</p>
            </div>
            
            <h2>发现的高危端口</h2>
            <div class="section">
            <table>
                <thead>
                    <tr>
                        <th width="10%">端口</th>
                        <th width="10%">协议</th>
                        <th width="10%">风险等级</th>
                        <th width="20%">服务版本 (Banner)</th>
                        <th width="50%">详情与脚本输出</th>
                    </tr>
                </thead>
                <tbody>
        """

        if not self.findings:
            html_content += f"""
                <tr>
                    <td colspan="5" style="text-align: center; color: green; font-weight: bold;">
                        <h3>✅ 未发现开放的高危端口或漏洞</h3>
                    </td>
                </tr>
            """
        else:
            for f in self.findings:
                risk_class = f"risk-{f['risk_level'].split(' ')[0]}"
                
                # 格式化脚本输出
                details = f"<p>{f['description']}</p>"
                if f['manual_verify']:
                    for v in f['manual_verify']:
                        details += f"<div style='color:red; font-weight:bold;'>[Python验证] {v}</div>"
                
                if f['scripts']:
                    details += "<pre>" + "\n".join(f['scripts']) + "</pre>"
                else:
                    details += "<p style='color:#7f8c8d'>[Nmap Script] 未返回明显漏洞信息。</p>"

                html_content += f"""
                    <tr>
                        <td>{f['port']}</td>
                        <td>{f['service_name']}</td>
                        <td><span class="tag {risk_class}">{f['risk_level']}</span></td>
                        <td>{f['banner']}</td>
                        <td>{details}</td>
                    </tr>
                """

        html_content += """
                </tbody>
            </table>
            </div>
            
            <h2>所有开放端口列表</h2>
            <div class="section">
            <table>
                <thead>
                    <tr>
                        <th width="15%">端口号</th>
                        <th width="20%">服务名称</th>
                        <th width="25%">产品信息</th>
                        <th width="25%">版本信息</th>
                        <th width="15%">状态</th>
                    </tr>
                </thead>
                <tbody>
        """

        if not self.all_open_ports:
            html_content += f"""
                <tr>
                    <td colspan="5" style="text-align: center; color: #7f8c8d;">
                        <p>未发现开放的端口</p>
                    </td>
                </tr>
            """
        else:
            for port_info in self.all_open_ports:
                html_content += f"""
                    <tr>
                        <td>{port_info['port']}</td>
                        <td>{port_info['service']}</td>
                        <td>{port_info['product']}</td>
                        <td>{port_info['version']}</td>
                        <td><span class="tag risk-中">{port_info['state']}</span></td>
                    </tr>
                """

        html_content += """
                </tbody>
            </table>
            </div>
            
            <div class="footer">
                <p>Generated by SentinelScan for Kali Linux</p>
                <p>⚠️ 仅用于授权安全测试，请勿用于非法用途</p>
            </div>
        </body>
        </html>
        """

        with open(self.html_report, 'w', encoding='utf-8') as f:
            f.write(html_content)
        return self.html_report

    def print_terminal_summary(self):
        """打印终端摘要"""
        print(f"\n{Colors.BOLD}=== 扫描结果摘要 ==={Colors.ENDC}")
        
        # 高危端口摘要
        print(f"\n{Colors.BOLD}高危端口发现:{Colors.ENDC}")
        if not self.findings:
            print(f"{Colors.GREEN}[*] 未发现重点关注的高危端口开放。{Colors.ENDC}")
        else:
            for item in self.findings:
                color = Colors.WARNING
                if "极高" in item['risk_level'] or "CRITICAL" in item['risk_level']:
                    color = Colors.FAIL
                
                print(f"{color}[!] 端口: {item['port']} ({item['service_name']}) | 风险: {item['risk_level']}{Colors.ENDC}")
                print(f"    版本: {item['banner']}")
                if item['manual_verify']:
                    print(f"    {Colors.FAIL}>>> 验证成功: {item['manual_verify'][0]}{Colors.ENDC}")
        
        # 所有开放端口摘要
        print(f"\n{Colors.BOLD}所有开放端口 ({len(self.all_open_ports)} 个):{Colors.ENDC}")
        if not self.all_open_ports:
            print(f"{Colors.GREEN}[*] 未发现开放的端口。{Colors.ENDC}")
        else:
            # 按端口号排序
            sorted_ports = sorted(self.all_open_ports, key=lambda x: x['port'])
            
            for i, port_info in enumerate(sorted_ports):
                service_info = f"{port_info['service']}"
                if port_info['product']:
                    service_info += f" ({port_info['product']})"
                if port_info['version']:
                    service_info += f" {port_info['version']}"
                
                print(f"  {i+1:2d}. 端口 {port_info['port']:5d} - {service_info}")

def main():
    print(f"""{Colors.BLUE}
    ███████╗███████╗███╗   ██╗████████╗██╗███╗   ██╗███████╗██╗
    ██╔════╝██╔════╝████╗  ██║╚══██╔══╝██║████╗  ██║██╔════╝██║
    ███████╗█████╗  ██╔██╗ ██║   ██║   ██║██╔██╗ ██║█████╗  ██║
    ╚════██║██╔══╝  ██║╚██╗██║   ██║   ██║██║╚██╗██║██╔══╝  ██║
    ███████║███████╗██║ ╚████║   ██║   ██║██║ ╚████║███████╗███████╗
    ╚══════╝╚══════╝╚═╝  ╚═══╝   ╚═╝   ╚═╝╚═╝  ╚═══╝╚══════╝╚══════╝xuekn
    {Colors.ENDC}{Colors.BOLD}SentinelScan V3.0 (Kali 2025 Edition){Colors.ENDC}
    {Colors.WARNING}企业级高危端口与漏洞自动化审计工具{Colors.ENDC}
    """)

    # 获取用户输入
    target = input(f"{Colors.BLUE}[+] 请输入目标IP或域名 (如: 192.168.1.1 或 example.com): {Colors.ENDC}").strip()
    
    if not target:
        print(f"{Colors.FAIL}[!] 错误: 目标不能为空{Colors.ENDC}")
        return
    
    # 创建输出目录
    output_dir = f"sentinel_scan_results_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
    
    print(f"\n{Colors.WARNING}[!] 警告: 建议以 Root 权限运行 (sudo python3 sentinel_scan.py) 以启用操作系统指纹检测和更准确的扫描。{Colors.ENDC}")
    print(f"{Colors.BLUE}[*] 开始扫描目标: {target}{Colors.ENDC}")
    print(f"{Colors.BLUE}[*] 报告目录: {output_dir}{Colors.ENDC}")
    print(f"{Colors.WARNING}[*] 按 Ctrl+C 可随时中断扫描{Colors.ENDC}")
    
    # 创建扫描器并运行
    scanner = SentinelScan(target=target, output_dir=output_dir)
    scanner.run_comprehensive_scan()
    
    # 打印终端摘要
    scanner.print_terminal_summary()

if __name__ == "__main__":
    main()